package com.xiashitech.agent.instrumentation.biz.api.increment;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.xiashitech.agent.cache.api.ApiCache;
// import com.xiashitech.agent.cache.api.extload.URITimedCache;
import com.xiashitech.agent.cache.api.extloadproxy.URITimedCacheProxy;
import com.xiashitech.agent.config.StartConfig;
import com.xiashitech.agent.constants.ErrorCode;
import com.xiashitech.agent.grpc.XsApiInfoClient;
import com.xiashitech.agent.instrumentation.httprequest.XSHttpServletRequestWrapper;
import com.xiashitech.agent.instrumentation.httprequest.XSHttpServletResponseWrapper;
import com.xiashitech.agent.utils.HttpUtil;
import com.xiashitech.agent.utils.JsonUtil;
import com.xiashitech.agent.utils.XSUtil;
import com.xiashitech.agent.utils.XiashiGrpcUtil;
import com.xiashitech.interfaces.agent.dto.AgentConfigDTO;
import com.xiashitech.interfaces.agent.dto.api.*;
import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.stream.Collectors;

public class ApiIncrementHelper {

    /**
     * 增量同步API信息 <br/>
     * - 新发现的API（执行新增操作，补齐API）<br/>
     * - 已存在的API (根据全量扫描捕获的出入参参数类型决定是否执行补齐出入参的动作，补齐参数)
     * @param request
     * @param response
     */
    public static void syncIncrementApi(HttpServletRequest request, HttpServletResponse response){
        String uri = request.getRequestURI();
        // 控制一个请求在一个时间段周期内只能触发一次参数补全
        if (XSUtil.isNotEmpty(URITimedCacheProxy.get(uri))){
            return;
        }
        Api api = ApiCache.get(uri);
        int pos = 0;
        while (Objects.isNull(api)) { // TODO 避免静态取到的 uri 和动态访问 uri 路径配置层级不同的问题，存在匹配到不是期望的 uri 上的问题
            pos = uri.indexOf('/', pos + 1);
            if(pos < 0)
                break;
            String newUri = uri.substring(pos);
            api = ApiCache.get(newUri);
        }
        boolean shouldUpdate = false;
        if (Objects.isNull(api)){
            // 在缓存中找不到 api ，只有是泛化 api 的情况，在 Controller 插桩中进行补全，这里不用管
            // api = initAPi(request);
            // ApiCache.add(uri, newApi);
            // 预留位置。只实现基于全量扫描的API的参数补全，对于运行时流量里新发现的API暂时不做处理。
            // 也有可能不处理，基于流量发现API和基于全量扫描API是2个不同的入口，都兼容混淆会导致业务复杂度升高。最好基于一个入口来做。
        }else {
            // 无法通过这个 uri 判断是否为泛化 uri，故在这里把 uri 加入缓存。过早加入可能在造成漏过对泛化 url 做补全的插桩点
            URITimedCacheProxy.add(uri);
            // 请求的 ContentType 大部分是没有的，检查 Accept 的类型
            if (StrUtil.containsAnyIgnoreCase(request.getContentType(), "json", "form")){
                // 更新 补齐参数
                if (shouldUpdateRequestParam(api)){
                    setRequestParams(request, api);
                    shouldUpdate = true;
                }
                // return;
            }

            if (StrUtil.containsAnyIgnoreCase(request.getHeader("accept"), "application/json", "*/*")){
                if (shouldUpdateResponseParam(api, response)){
                    setResponseParams(response, api);
                    shouldUpdate = true;
                }
                // return;
            }

            if (shouldUpdate) doAsyncCall(api);
        }
    }
    public static void syncIncrementApiByGenericType(Object requestparam, Object responseparam, String path) {
        if(path.lastIndexOf('{') < 0 && path.indexOf('}') < 0) {
            System.out.println("syncIncrementApiByGenericType C, not generic path: " + path);
            return;
        }
        if(!requestparam.getClass().isAssignableFrom(ServletWebRequest.class)) {
            System.out.println("syncIncrementApiByGenericType A: " + requestparam.getClass().getName());
            return;
        }
        if(!responseparam.getClass().isAssignableFrom(XSHttpServletResponseWrapper.class)) {
            System.out.println("syncIncrementApiByGenericType B: " + responseparam.getClass().getName());
            return;
        }
        // 换容器，如换 Tomcat 为 Undertow 后，以下是否能取对？ TODO
        // ServletWebRequest request = (ServletWebRequest)requestparam;
        HttpServletRequest request = (HttpServletRequest)((ServletWebRequest)requestparam).getNativeRequest();
        HttpServletResponse response = (HttpServletResponse)responseparam;
        try {
            //Method method = nativeRequest.getClass().getDeclaredMethod("getRequestURI");
            //String uri = (String) method.invoke(nativeRequest);
            String uri =request.getRequestURI();
            System.out.println("syncIncrementApiByGenericType D, generic url: " + path + ", realUrl: " + uri);
            // System.out.println("syncIncrementApiByGenericType E, uri: " + uri);
            // System.out.println("syncIncrementApiByGenericType H, nativeRequest: " + ((ServletWebRequest)requestparam).getNativeRequest().getClass().getName()+ ", " + request.getClass().getName());
            if (XSUtil.isNotEmpty(URITimedCacheProxy.get(uri))) {
                return;
            }
            URITimedCacheProxy.add(uri);
        } catch (Exception e) {
            StartConfig.throwableStackPrinter(e);
            return;
        }
        // 只处理泛化的 path，取不到就返回了
        Api api = ApiCache.get(path);
        if (Objects.isNull(api)) {
            System.out.println("syncIncrementApiByGenericType F, generic url: " + path);
            return;
        }

        boolean shouldUpdate = false;
        // 请求的 ContentType 大部分是没有的，检查 Accept 的类型
        if (StrUtil.containsAnyIgnoreCase(request.getContentType(), "json", "form")){
            // 更新 补齐参数
            if (shouldUpdateRequestParam(api)){
                setRequestParams(request, api);
                shouldUpdate = true;
            }
        }
        String acceptStr = request.getHeader("accept");
        // System.out.println("syncIncrementApiByGenericType G, accept: " + acceptStr);
        if (StrUtil.containsAnyIgnoreCase(acceptStr, "application/json", "*/*")){
            if (shouldUpdateResponseParam(api, response)){
                setResponseParams(response, api);
                shouldUpdate = true;
            }
        }

        if (shouldUpdate) doAsyncCall(api);
    }

    private static void setRequestParams(HttpServletRequest request, Api api){
        // 1. 先取requestParams（包含 form 表单参数和Url拼接参数）
        Enumeration<String> parameterNames =  request.getParameterNames();
        List<Params> requestParams = new ArrayList<>();
        while (parameterNames.hasMoreElements()){
            Params params = Params.builder().type("String").name(parameterNames.nextElement()).build();
            requestParams.add(params);
        }
        //2. 如果请求头 ContentType 是JSON,则再接着解析请求体Body的参数
        if (StrUtil.containsIgnoreCase(request.getContentType(), "json") && request instanceof XSHttpServletRequestWrapper){
            //提取JSON BODY 参数
            XSHttpServletRequestWrapper xsRequest = (XSHttpServletRequestWrapper) request;
            String jsonBody = new String(xsRequest.getBody());
            Set<String> keySet = new HashSet<>();
            //提取所有叶子节点KEY
            XSUtil.getJsonKeys(jsonBody, keySet);
            keySet.forEach(key ->{
                //识别参数type类型开销较大，性价比较低，统一设置为String. 如果后台逻辑需要，可以解析JSON识别。
                int pos = key.lastIndexOf('/');//.indexOf(':');
                String path = "";
                String leafkey = key;
                if(pos > 0) {
                    path = key.substring(0, pos);
                    leafkey = key.substring(pos+1);
                }
                Params params = Params.builder().type("String").name(leafkey).build();
                requestParams.add(params);
            });
        }
        //合并API原参数
        requestParams.addAll(api.getRequestParams());
        api.setRequestParams(requestParams.stream().distinct().collect(Collectors.toList()));
    }

    private static void setResponseParams(HttpServletResponse response, Api api){
        if (StrUtil.containsIgnoreCase(response.getContentType(), "json") && response instanceof XSHttpServletResponseWrapper) {
            List<Params> responseParams = new ArrayList<>();
            XSHttpServletResponseWrapper xsResponse = (XSHttpServletResponseWrapper) response;
            String jsonBody = xsResponse.getJsonBody();
            Set<String> keySet = new HashSet<>();
            //提取所有叶子节点KEY
            XSUtil.getJsonKeys(jsonBody, keySet);
            keySet.forEach(key -> {
//                //识别参数type类型开销较大，性价比较低，统一设置为String. 如果后台逻辑需要，可以解析JSON识别。
//                int pos = key.indexOf(':');
//                String path = "";
//                String leafkey = key;
//                if(pos > 0) {
//                    path = key.substring(0, pos);
//                    leafkey = key.substring(pos+1);
//                }
                // 对于菜单类 json，数据结构多层嵌套，会出现 name 相同而 keyPath 不同的数据，以下语句无法对这类数据消重，会报错
                // TODO 暂时按如下逻辑消重，报错就维持原数据
                // 处理 leafkey 相同，path 不同时：
                // 有 path 的覆盖无 path 的
                // 都有 path 时，直接越过不加入
//                boolean isSkip = false;
//                Iterator<Params> paramsIterator = responseParams.iterator();
//                while(paramsIterator.hasNext()) {
//                    Params p = paramsIterator.next();
//                    if(leafkey.equals(p.getName())) {
//                        if( p.getKeyPath() != null && !p.getKeyPath().isEmpty()) {
//                            isSkip = true;
//                        } else {
//                            paramsIterator.remove();
//                        }
//                        break; // 应该 responseParams 中没有重复的，只会有一次遇到相同的
//                    }
//                }
//                if(!isSkip) {
                //Params params = Params.builder().type("String").name(leafkey).keyPath(path).build();
                Params params = Params.builder().type("String").name(key).build();
                responseParams.add(params);
//                }
            });
            //合并API原参数
            // 使用简单的 addAll，会出现数据类型不同的同名 key 值，下一步 distinct 无法去重，提交 metadata 会报错，
            // responseParams.addAll(api.getResponseParams());
            List<Params> originalApiResponseParams = api.getResponseParams();
            for(Params op : originalApiResponseParams) {
                String pname = op.getName();
                boolean isExist = false;
                for(Params newParams : responseParams) {
                    if(pname.equals(newParams.getName())) {
                        isExist = true;
                        break;
                    }
                }
                if(!isExist) {
                    responseParams.add(op);
                }
            }
            api.setResponseParams(responseParams.stream().distinct().collect(Collectors.toList()));
        }
    }

    private static boolean shouldUpdateRequestParam(Api api){
        boolean should = false;
        List<Params> paramsList = api.getRequestParams();
        if (CollUtil.isEmpty(paramsList)) return should;
        for (Params params : paramsList) {
            if (StrUtil.containsAnyIgnoreCase(params.getType(), "map", "object", "array")){
                should = true;
                break;
            }
        }
        return should;
    }

    private static boolean shouldUpdateResponseParam(Api api, HttpServletResponse response){
        boolean should = false;
        List<Params> paramsList = api.getResponseParams();
        if (CollUtil.isEmpty(paramsList)) return should;
        for (Params params : paramsList) {
            if (StrUtil.containsAnyIgnoreCase(params.getType(), "map", "object", "array", "string")){
                should = true;
                break;
            }
        }
        return should;
    }

    private static void doAsyncCall(Api api) {
        new Thread(() -> {
            try {
                ApiRequest apiRequest = ApiRequest.builder().systemId(StartConfig.getParamValueFromKey(StartConfig.systemId)).serviceName(InstrumentationConfig.get().getString("otel.service.name"))
                        .apis(Arrays.asList(api)).build();
                if(XiashiGrpcUtil.useGrpc()) {
                    XsApiInfoClient.getClient().grpcApiParasToOMD(apiRequest);
                } else {
                    String param = JsonUtil.convertObjectToString(apiRequest);
                    String url = AgentConfigDTO.getConfig().getApiParamService();
                    if (XSUtil.isEmpty(url)){
                        XSUtil.debug("Request 请求时增量同步API出入参失败（没有获取到 ApiParamService 接口地址）");
                        return;
                    }
                    String response = HttpUtil.doPost(url, param, "application/json;charset=UTF-8");
                    if (response != null && !response.isEmpty() && JsonUtil.isJson(response)) {
                        ApiResponse apiResponse = JsonUtil.convertStringToObject(response, ApiResponse.class);
                        if (apiResponse.getCode() != null && apiResponse.getCode().equals(ErrorCode.RESULT_1001.getCode())) {
                            XSUtil.debug("Request 请求时增量同步API出入参成功");
                        } else {
                            XSUtil.debug("Request 请求时增量同步API出入参失败 (状态码错误)");
                        }
                    } else {
                        XSUtil.debug("Request 请求时增量同步API出入参失败（请求错误）");
                    }
                }
            } catch (Throwable e) {
                StartConfig.throwableStackPrinter(e);
            }
        }).start();
    }

}
